package arconplugin.handlers;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;

import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Path;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.ecore.resource.ResourceSet;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.jface.viewers.LabelProvider;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.uml2.uml.Model;
import org.eclipse.uml2.uml.Stereotype;
import org.eclipse.uml2.uml.UMLPackage;

public class ConnectFilesHandler extends SetupArconHandler {
	
	protected final String uml_extension = "uml";
	private final String save_path_string = "related_model=";
	private final String save_type_string = "related_model_isSystem=";
	
	protected interface ModelType {
		public abstract boolean isSystemModel();
		public abstract IPath getPath();
		public abstract String getCompletePath();
		public abstract boolean exists();
		public abstract void setPath(String path_str);
		public abstract void setPath(IPath path);
	}
	protected abstract class BaseModelType implements ModelType{ 
		protected IPath path = null;
		public IPath getPath() {
			return path;
		}
		public void setPath(String path_str) {
			this.path = new Path(path_str);
		}
		public void setPath(IPath path) {
			this.path = path;
		}
		public String getCompletePath() {
			if(getPath()==null)
				return null;
			return getPath().addFileExtension(uml_extension).toOSString();
		}
		public boolean exists() {
			String file = getCompletePath();
			if(file==null)
				return false;
			return (new File(file).exists());
		}
	}
	protected class ArchRulesModel extends BaseModelType {
		
		@Override
		public String toString() {
			return "Architecture Rules Model";
		}

		@Override
		public boolean isSystemModel() {
			return false;
		}
	}
	protected class SystemModel extends BaseModelType {
		@Override
		public String toString() {
			return "System Model";
		}

		@Override
		public boolean isSystemModel() {
			return true;
		}
	}
	
	@Override
	public Object execute(ExecutionEvent event) throws ExecutionException {
		IFile file = getActiveFile();
		if(file==null) {
			debugPrint("No Active File");
			return null;
		}
		SetupConnectedModel(file.getLocation().removeFileExtension()); //must remove file extension or else we may get the path to the diagram
		return null;
	}
	
	/**
	 * Get Active File in the editor. Should be one of the models that we want to check, i.e. Architecture or System model.
	 * @return Active file or null if no file is open in editor
	 */
	protected IFile getActiveFile()
	{
		IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
	    if (window == null) {
	    	debugPrint("window null");
	    	return null;
	    }

	    IEditorPart editorPart = window.getActivePage().getActiveEditor();
		if(editorPart == null) {
			debugPrint("No editorPart");
			return null;
		}
		
		IEditorInput input = editorPart.getEditorInput();
		if(!(input instanceof FileEditorInput)) {
			debugPrint("not FileEditorInput");
			return null;
		}
		
		return ((FileEditorInput)input).getFile();    
	}	

	/**
	 * Get the pre-set path or let the user select the path if it doesn't exist
	 * @return ModelType with path or null if no correct path was given
	 */
	protected ModelType GetConnectedModel(IPath first_model) {
		ModelType model = LoadConnectedModel(first_model);
		if(model == null || !model.exists()) {
			model = SetupConnectedModel(first_model);
		}		
		return model;
	}
	
	/**
	 * Give a dialog to select the model that should be checked together with the key_model
	 * @param key_model
	 * @return
	 */
	private ModelType SetupConnectedModel(IPath key_model) {
		ModelType connected_model = getOppositeModelType(key_model);
		if(connected_model!=null) { //got which type it is
			SelectModel(key_model, connected_model);
		}
		else
		{
			//Could not figure out which type the current model is (so we know what the related model should be), 
			//ask the user what type the related model should be instead.
			ElementListSelectionDialog dialog = new ElementListSelectionDialog(getShell(), new LabelProvider());
			dialog.setTitle("Model Type Selection");
			dialog.setMessage("Select type of the other model to check against:");
			dialog.setElements(new Object[] { new ArchRulesModel(), new SystemModel() });
	
			if( dialog.open()== IStatus.OK){ // open dialog and wait for return status code.
				Object[] value = dialog.getResult();
				debugPrint(value[0].toString());
				
				connected_model = (ModelType)value[0];
				SelectModel(key_model, connected_model);
			}
		}
		return connected_model;
	}
		
	/**
	 * Given the model path, open the model and check if there exist any stereotypes that tells which type of model it is.
	 * Looks for the stereotypes defining System Model or Arch Rules Model. Returns the opposite ModelType of the found type for key_model.
	 * @param key_model 
	 * @return ModelType of the related model, or null if the current ModelType could not be found.
	 */
	private ModelType getOppositeModelType(IPath key_model) {
		Model model = getModel(key_model.addFileExtension(uml_extension).toOSString());
		
		if(model==null) {
    		debugPrint("model == null");
    		return null;
    	}
		
    	for(Stereotype s : model.getAppliedStereotypes()) {
    		debugPrint("Had stereotype:"+s.getName());
    		if(s.getName().equals("Arch_Meta_Model")) {
    			debugPrint("found Arch_Meta_Model Stereotype");
    			return new SystemModel();
    		}
    		if(s.getName().equals("System_Model")) {
    			debugPrint("found System_Model Stereotype");
    			return new ArchRulesModel();
    		}
    	}
		debugPrint("Found no System_Model or Arch_Meta_Model stereotype");
		return null; 	
	}

	/**
	 * Give a dialog to select the model that should be checked together with the key_model
	 * @param key_model
	 * @param connected_model - model type for which the path will be selected
	 */
	private void SelectModel(IPath key_model, ModelType connected_model) {
		//If ArCon wasn't found, open a FileDialog for the user to select it
		FileDialog fileDialog = new FileDialog(getShell());
		String[] ext = {"*.uml"};
		fileDialog.setFilterExtensions(ext);
		fileDialog.setText("Select the Model to check against");
		fileDialog.setFilterPath(key_model.removeLastSegments(1).toOSString()); //set default folder in the same folder as key_model
		String model_path = fileDialog.open();
		
		if (model_path != null) {//A check to see so that a file were chosen
			IPath path = new Path(model_path).removeFileExtension(); //always saving path without extension
			connected_model.setPath(path);
			//save values
			SaveConnectedModel(key_model, connected_model); 
		}
	}
	
	/**
	 * Save the model connected to the key model
	 * @param key_model - string of the model which defined the key (typically system_model path)
	 * @param connected_model
	 */
	private void SaveConnectedModel(IPath key_model, ModelType connected_model) {
		IPath save_file = getSavedInfoFilePath(key_model);
		//TODO: change this to a more sophisticated system, for example an XML structure 
		try{
			FileWriter fstream = new FileWriter(save_file.toOSString());
			BufferedWriter out = new BufferedWriter(fstream);
			//Save relative path
			debugPrint("Saving to file: "+ save_file.toOSString());
			debugPrint(save_path_string+connected_model.getPath().makeRelativeTo(key_model).toString()+"\n");  
			debugPrint(save_type_string+connected_model.isSystemModel()+"\n"); 
			
			out.write(save_path_string+connected_model.getPath().makeRelativeTo(key_model).toString()+"\n");  
			out.write(save_type_string+connected_model.isSystemModel()+"\n");  
			out.close(); //Close the output stream
	  }catch (Exception e){//Catch exception if any
		  printError("Error: " + e.getMessage());
	  }
	}

	/**
	 * Get the associated model given the key model
	 * @param key_model
	 * @return connected model
	 */
	private ModelType LoadConnectedModel(IPath key_model) {
		if(!SavedInfoFileExist(key_model)) { //no file to load settings from
			return null;
		}
		
		ModelType connected_model = null;
		IPath load_file = getSavedInfoFilePath(key_model);
		boolean isSystemModel = false;
		IPath path = null;
		
		//read the file
		try {
			BufferedReader reader = new BufferedReader(new FileReader(load_file.toOSString()));
		    String line = null;
		    while ((line = reader.readLine()) != null) {
		        debugPrint("Reading: ["+line+"]");
		        if(line.startsWith(save_path_string)) {
		        	path = new Path(line.substring(save_path_string.length())); //get value without the prefix
		        	if(path.isAbsolute()==false) {
		        		//append to original key_model path since the saved path was relative to the key_model path
		        		path = key_model.append(path); 
		        	}
		        	debugPrint("New path: "+path.toString());
		        }
		        else if(line.startsWith(save_type_string)) {
		        	isSystemModel = Boolean.parseBoolean(line.substring(save_type_string.length())); //get value without the prefix
		        	//debugPrint("Is System Model: "+String.valueOf(isSystemModel));
		        }
		    }
		} catch (Exception e) {
			printError("Error: " + e.getMessage());
			return null;
		}
		
		//now create the modelinfo based on what we read
		if(isSystemModel) {
			connected_model = new SystemModel();
		} else {
			connected_model = new ArchRulesModel();
		}
		
		connected_model.setPath(path);
		return connected_model;
	}
	
	/**
	 * return if there exist a file for saved info about the given key_model
	 * @param key_model - model to check if a saveInfo file exist for.
	 * @return
	 */
	protected boolean SavedInfoFileExist(IPath key_model) {
		IPath path = getSavedInfoFilePath(key_model);
		if((new File(path.toOSString())).exists())
			return true;
		return false;
	}
	
	/**
     * Load a model given the path
     * @param path - model path
     * @return Model or null
     */
    protected Model getModel(String path) {
    	try {
	    	URI typesUri = URI.createFileURI(path);
			
			ResourceSet set = new ResourceSetImpl();
	        Resource r = set.getResource(typesUri, true);
	        return (Model)EcoreUtil.getObjectByType(r.getContents(), UMLPackage.Literals.MODEL);
    	} catch(Exception e) {
    		debugPrint("Error Loading Model\n"+e.getMessage());
    		return null;
    	}
    }
    
	/**
	 * Returns the complete file path to the saveInfo file for the key_model
	 * @param key_model
	 * @return Path including file extension
	 */
	private IPath getSavedInfoFilePath(IPath key_model) {
		return key_model.addFileExtension("arcon");
	}
}
